iT邦幫忙

2026 iThome 鐵人賽

DAY 7
0
Build on Google AI

LOCAL:30 天打造 LINE × Google AI 地方服務 Agent系列 第 7 篇

Day 7|看懂不等於可信:來源、版本與不知道

  • 分享至 

  • xImage
  •  

主辦傳來新海報:「時間改了,請以這張為準!」今天用 Gemini 整理教學修訂圖,再由 Python 對上同一場活動、標出時間差異,核對後讓查詢改用新版。換了圖片檔,活動資訊就一定有變嗎?我們實際跑一次更新流程。

Day 7 adds version-aware updates to LOCAL’s poster workflow. We reuse the reviewed Day 6 data, extract a clearly labeled teaching revision with Gemini, match stable event identities, and compare values, quotations, and statuses. A human reviews the changes before adoption. Queries show the previous, pending, and adopted states, while a separate announcement can supply a missing year without rewriting the image evidence.

本次新舊海報與查詢結果

新來源性質:教學用修訂版/非主辦公告。舊圖引用前篇實測;以下新圖與查詢來自本次紀錄。

LOCAL 欄位差異引導複核
圖 1:左為 Day 6 原圖,右為教學修訂圖,八場共用的活動時段由 07:30~11:00 改為 08:00~11:00。畫面底部顯示本次 9 筆三元組差異與 27 個待核對項目;逐欄內容可在完整複核頁面查看。

這次比較得到 9 個欄位三元組差異(三元組 = value / quote / status = 值 / 原文 / 狀態);需核對的變動與連動欄位共 27 個。這是本次檔案的差異數,不是模型正確率。

LOCAL 三階段查詢結果對照
圖 2:同樣以花壇查詢,活動時間依序呈現 07:30~11:00、待確認、08:00~11:00。狀態 pending_review(新版待核)表示新版資料已出現,但尚待人工核對,因此受影響的舊值暫時不當成最新答案;待核時保留活動名稱,將受影響的時間與連動日期暫列待確認。

階段 查詢狀態 活動 活動日期 活動時間
更新前 ok 卦山大縱走 彰化兜兜圈 2026-09-19 07:30~11:00
新版待核 pending_review 卦山大縱走 彰化兜兜圈 待確認 待確認
採用新版後 ok 卦山大縱走 彰化兜兜圈 2026-09-19 08:00~11:00

註:八場時段同步變動,表中以花壇為例。

實驗條件 本次紀錄
模型/思考等級 gemini-3.8-flash/LOW
新圖擷取耗時(秒) 7.676
新圖輸入/回覆/思考 token 1386/2756/紀錄為 null(回應未提供)
原圖/新圖 SHA-256(前 12 碼) 47407ae3de5c/f3e766619e64
本機查詢 API 呼叫 三階段均為 0

我的判讀:修訂圖把時段改成 08:00~11:00,Gemini 讀出新時段與原文;配對後 8 場的時間標為變動,另有 1 筆擷取範圍微差(主場標示仍存在);待核查詢暫不提供受影響的舊時段,採用後查到新時段。

本次另以公告依據補足 8 場的年份;原圖三元組保留,完整日期與公告原文分開保存。

一、這一篇,從讀者的一個問題開始

Day 6 發表後,有讀者問:主辦更新同一張海報,要用圖片雜湊建立新版,還是只把有變動的欄位交給人複核?

問得好。昨天我們才讓 LOCAL 學會看海報,今天就要面對主辦最熟悉的一句話:「不好意思,請以這張為準!」

我的選擇是兩者分工:圖片雜湊辨認收到哪份來源,欄位差異引導人工看重點。中間還有一件容易漏掉的事:先確認新舊資料指的是同一場活動。

這篇沿用昨天的原圖與已核對結果,再建立一張醒目標示「教學用修訂版/非主辦公告」的副本。海報上八場共用同一行時段,所以只改這一行,八場的時間就一起變了;修訂框還蓋掉了「週六」兩個字——這件事是模型在 notes 裡先提醒我的。修改的是測試素材,官方原圖保持原樣。

完整流程是:

原圖與核對資料 → 教學修訂圖 → Gemini 整理 → 場次配對 → 欄位差異 → 人工核對 → 採用新版 → 再查一次。

Gemini 負責看新圖,依 Structured Outputs(結構化輸出:先定義欄位結構,讓 Gemini 按這個結構回傳資料,而不是只有自由文字,方便後續 Python 比較)整理出和昨天相同的欄位,Python 才有一致的資料可以比較。原圖重用 Day 6 的真實結果,新圖採同樣的欄位提示;兩張圖各有模型紀錄,今天只需新增一次圖片呼叫。[1][2]

二、三種身分分開,才知道究竟更新了什麼

先別急著比較 JSON。這個流程裡有三種身分,各自回答不同的問題:

身分 問題 本篇做法
來源圖片 這是不是同一份檔案? 保留原始位元組與 SHA-256
活動場次 新舊資料是不是同一場? 第一次匯入時指派穩定 ID,更新沿用
資料版本 查詢現在採用哪一份? 保存上一版、候選與人工採用紀錄

SHA-256 計算的是檔案位元組(就像檔案的數位指紋)。主辦只調整壓縮方式,圖片內容看起來相同,摘要值也可能改變;這時值得留下新的來源紀錄,活動日期卻未必需要重填。[3]

Day 6 的 ID 用圖片雜湊加場次序號,適合標記「這張圖中的第幾筆」。這次匯入時指派一個 evt-... 識別,往後更新都沿用它。名字、日期或圖片改動,屬於活動的內容變動,而不是重新算一個活動身分。

MATCH.html 將新圖場次與既有活動並排。同名且只有一筆時先提供配對建議,最後由人確認;重名、改名或新增場次,則直接選擇對應項目。

陣列位置只用來指向這次輸入,活動 ID 才負責跨版本連接。 把 A、B 兩場的排序換成 B、A,這個區別就很有感了。

三、昨天少的年份,今天讓來源說清楚

前一篇的海報有月日,完整日期仍缺年份。今天另外保存對應公告的網址與原文,先確認同一屆、同一場活動,再補足可查詢的日期。

這裡我把兩種證據分開放。以下是欄位安排的示意,實際值以本次核對紀錄為準:

{
  "fields": {
    "date": {
      "value": null,
      "quote": "09.19",
      "status": "unclear"
    }
  },
  "enrichments": {
    "date": {
      "value": "2026-09-19",
      "method": "reviewed_notice_year_plus_image_month_day"
    }
  }
}

這段安排保留圖片原本提供的資訊,完整日期則有另外一份公告支持。 本次採用的公告依據為彰化縣政府新聞稿(詳見文末參考資料 [5]),實際 enrichments.date 保存公告原文、來源網址、取得時間與核對者。

程式會檢查年份、月日、步道名稱與引文是否相符,人工確認的重點則是「公告和圖片是不是同一屆、同一場」。如果兩個來源真的寫了不同日期,程式會要求先處理衝突,不能只因某個檔案比較晚取得,就認定它比較正確。

公告的發布時間與我們取得資料的時間也分開保存。前者描述主辦何時公布,後者描述系統何時收到;缺少發布時間時,仍可保留網址和取得時間。

查詢會優先使用經核對的補充日期,原圖的 null 與月日原文繼續留著。這樣就能回答「日期從哪裡來」,而讓補上的年份也找得到依據。

第一次貼進來的公告文字有四行對錯步道,程式只比月日就放行了,我也按了確認。加上步道名稱比對之後,它才把四行擋下來。看懂不等於可信,連核對的人也適用。

四、先做出一張修訂圖,再讓 Gemini 讀一次

本篇程式在 examples/day07/,相依 Day 5 的搜尋函式與 Day 6 的圖片擷取。從 Repo 根目錄開始:

PY=examples/day05/.venv/bin/python
$PY examples/day07/verify.py --sdk
$PY examples/day07/demo.py

第一個指令檢查版本流程與 SDK 組態;第二個用明確標示的離線資料演練「更新前、待核、採用後」。已有前篇環境就直接沿用,新環境的完整準備方式在本篇 README。

接著匯入 Day 6 的 catalog.reviewed.json、review.audit.json 與原圖擷取目錄。程式先核對它們的雜湊關係,再列出新的穩定活動 ID。海報缺年份時,在這一步完成上一節的公告核對。

修訂圖用瀏覽器開啟 revision_editor.html 製作:選原圖、框住一個原有時段、填新文字,再下載圖片與 edit.json。工具自動加上教學標示,也記錄修改區域及前後圖片的雜湊。

選好的修改要單純。例如原本寫活動時段,就只改活動時段;留著其他文字,才容易觀察 Gemini 有沒有找對地方。

依 README 設定 $SESSION 與圖片位置後:

$PY examples/day07/run.py extract --session "$SESSION" --live \
  --image "$REVISION_IMAGE" --edit-manifest "$EDIT_JSON" \
  --source-kind teaching_revision \
  --source-ref "教學修訂副本,原公告來源已另存" \
  --env-file "$GEMINI_ENV" --origin author_local

這幾行重用 Day 6 的 GenerateContent 呼叫,只把新圖送進同一套擷取流程。 模型、LOW、生成上限與提示保存在原始紀錄;如果回覆未通過欄位驗證,先看這次錯在哪裡,再決定是否重試。

取得新圖欄位後,依序打開 MATCH.html 確認場次,再由 plan 產生 REVIEW.html。模型負責讀圖,哪兩筆是同一場、哪些變動要採用,現在都有看得見的操作位置。

五、只比 value 還不夠,原文與狀態也要一起看

Day 6 每個欄位都保存「值/原文/狀態」(程式欄位為 value/quote/status,quote 代表海報上支持這個值的原文),今天正好派上用場。versioning.py 的比較核心如下:

def triple_diff(left: dict, right: dict) -> list[str]:
    return [k for k in ("value", "quote", "status")
            if type(left[k]) is not type(right[k])
            or normalize(left[k]) != normalize(right[k])]

這幾行找出哪個部分改了,而不只回答整份 JSON 是否相同。 normalize() 只整理字串首尾空白與 Unicode 組成形式,原始引文仍然保存。

例如,時間值變了,是內容變動;值相同但支持它的原文改了,是依據變動;原本 not_shown 變成 stated,則是新取得的資訊。三者都值得人看一眼。這次 9 筆差異中,8 筆是活動時間的值變了;另 1 筆出在二水登廟步道:值仍是「登廟步道」,兩次擷取的原文卻從「登廟步道 主場」變成「登廟步道」。回頭看圖,「主場」標示在兩張圖上其實都還在,因此這筆是模型摘錄範圍的微差,而非海報刪掉了這個標示——三元組比對除了能抓出活動異動,連模型兩次擷取的微小邊界差異也能如實浮現。狀態變動本次沒有出現。

9 算的是有差異的欄位三元組,27 算的是連同關聯欄位一起核對的去重項目:8 場時間變動各帶出 time/date/meeting_time 3 個連動欄位(共 24 項),加上二水那場的 venue/area/meeting_point 3 個連動欄位(共 3 項),合計 24 + 3 = 27。同一欄位的值與引文都變了,仍算一筆欄位差異。

複核也要考慮欄位之間的關係。活動時間改了,日期和集合時間一起帶出來;集合地點改了,場地與鄉鎮也一起看。REVIEW.html 會將「直接變動」與「連動核對」標開,讓人知道這一格為什麼出現。

未變欄位可參照之前的核對紀錄,新圖片仍留下本次原圖確認。要是模型漏掉新增的小字,頁面也能額外勾選欄位、修正並記錄原因;人工補改的欄位也會套用同一套連動核對,確保不會只改時間而漏看日期。差異是幫人找到重點,不是取代看圖。

六、新版還沒核對,搜尋該回答哪個時間?

這裡的選擇會直接影響使用體驗。舊時間昨天核對過,但現在新圖顯示不同的時段,就不能繼續把它當成最新資訊。

待核時,程式把受影響欄位的值設為 null,在 pending_fields 列出欄位名稱,並用 pending_review 標示更新狀態。其他已核對資訊(如活動名稱、地點)仍可正常查詢。這次日期本身沒有改,但它與活動時間屬於同一組連動核對,為避免湊出前後不一致的時段,會連同活動時間一併暫列為「待確認」。

還有一個小陷阱:如果日期從週六改成週日,只拿舊目錄篩選,問週日的讀者可能得到「查無」。這次待核查詢會看舊值與候選值的聯集,把這場活動列為待確認,而不是讓它從查詢中消失。

使用同一個問題,依序留下三份結果:

$PY examples/day07/run.py query --session "$SESSION" \
  --phase before --area 花壇
$PY examples/day07/run.py query --session "$SESSION" \
  --phase pending --area 花壇
# 完成 REVIEW.html 並以 apply 採用 decision.json 後:
$PY examples/day07/run.py query --session "$SESSION" \
  --phase after --area 花壇

這三次都呼叫既有 Python 搜尋工具,不再花模型請求。 採用後,catalog.after.json 保存新資料,adoption.json 記下核對者與差異,active.json 指向本機目前的採用版本。

採用前還會檢查複核計畫與資料版本相符。若有人在中途換了基準資料,舊的勾選就不能直接套上去。這個版本綁定,也正好成為下一篇操作確認的基礎。

七、三個小實驗,分清檔案、活動與答案

離線對照 觀察什麼? 教會的原理
圖片位元組變了,欄位保持相同 來源 hash 不同,欄位差異可為零 檔案換版與活動內容變更分開
A、B 兩場順序交換 確認配對後仍是原來的活動 穩定 ID 與陣列位置分開
時間改了,或原文/狀態改了 直接變動與連動欄位一起複核 差異分類與人工採用分開

這些是程式附的離線案例;新舊海報的真實 Gemini 結果,則看前面的實測紀錄。若模型對新圖讀錯,人可以修正候選並保留原始輸出,這也是工作流程需要留下的位置。

有人可能會問:這樣算 RAG 或 Grounding 嗎?簡言之,RAG(Retrieval-Augmented Generation,檢索增強生成)是先取回相關資料,再提供給模型生成回答;Grounding 則強調回答有可追溯的外部依據。先抓住本篇真正完成的部分:整理有來源、有版本的資料,再讓工具查到適合使用的內容。這是在準備可依據資料回答的檢索端;把工具結果交回模型生成回答,是後續整合的下一步。

來源網址有助追溯,具體引文則用來核對某個答案。Google 的 Grounding with Google Search 另有工具設定與回傳的 grounding metadata,本篇使用既有海報與人工保存的公告,沒有啟用那項搜尋工具。[4]

八、我的取捨:模型重讀一張圖,人少翻一大份 JSON

我選擇先讓 Gemini 重新讀整張修訂圖,再由 Python 整理差異。這樣仍能看見跨欄位與整張版面的資訊,人則把注意力放在真正改動及連動項目。

代價是新圖仍需一次擷取,場次配對也保留人工確認;好處是先把更新到採用的流程做清楚。要不要改成只讀圖片局部,可以等有了品質、耗時與成本紀錄,再比較是否划算。

這一版專心完成單機、單一核對者的更新流程;多個人同時審資料、跨服務發布版本與自動掃描網站,留給各自需要的場景。就算差異為零,也要看一次新圖有沒有新增的小字公告,避免兩次擷取都漏掉同一段小字。

今天帶走的方法,是讓來源可追溯、活動能對上、差異能複核,最後讓查詢知道該用哪一版。 回頭看那則留言,「用雜湊,還是比欄位」的答案,已經變成一段可以跑的流程。

下一篇接著問:LOCAL 查到新版本後,使用者回了一個「好」,究竟同意的是舊時間,還是新時間?我們會把這個「好」綁到具體操作與版本。

你在更新活動資料時,最怕漏掉日期、集合地點,還是臨時取消通知?歡迎分享你的情境。

程式與參考資料

本篇操作與完整程式:LOCAL 的 examples/day07。本篇首發程式基準:472f75f,前篇已公開的依賴基準:a73571c19af50ca66431f5e138183c8c24d82f6d,各篇完整操作方式與執行紀錄格式均保存在對應範例。

前篇:Day 6|從活動海報到服務資料。


上一篇
Day 6|從活動海報到服務資料
下一篇
Day 8|一句「好」不夠:確認綁定具體操作
系列文
LOCAL:30 天打造 LINE × Google AI 地方服務 Agent 共 12 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言